/*
 * Copyright 2007 Agile Plugins Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.agileplugins.monkey;

import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInspection.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class InstanceProviderCheckingInspection extends LocalInspectionTool {
    protected Logger logger = Logger.getInstance(getClass().getName());

    @NotNull
    public String getGroupDisplayName() {
        return "PurpleMonkeyDishwasher";
    }

    @NotNull
    public String getDisplayName() {
        return "Check InstanceProvider calls";
    }

    @NotNull
    public String getShortName() {
        return "InstanceProviderChecker";
    }

    public boolean isEnabledByDefault() {
        return true;
    }

    @NotNull
    public HighlightDisplayLevel getDefaultLevel() {
        return HighlightDisplayLevel.ERROR;
    }

    public ProblemDescriptor[] checkMethod(@NotNull PsiMethod method, @NotNull InspectionManager manager, boolean isOnTheFly) {
        PsiManager psiManager = method.getManager();
        Project project = manager.getProject();
        GlobalSearchScope globalsearchscope = GlobalSearchScope.allScope(project);
        PsiClass aClass = psiManager.findClass("au.com.suncorp.easydoc.util.instance.InstanceProvider", globalsearchscope);
        if (aClass == null) {
            logger.info("Can't find instanceProvider");
            return null;
        }
        PsiMethod[] methods = aClass.getMethods();
        List<ProblemDescriptor> list = new ArrayList<ProblemDescriptor>();
        for (PsiMethod psiMethod : methods) {
            if (psiMethod.getName().equals("newInstance")) {
                Collection<PsiReference> references = ReferencesSearch.search(psiMethod, new LocalSearchScope(method), true).findAll();
                for (PsiReference reference : references) {
                    if (reference instanceof PsiReferenceExpression) {
                        PsiReferenceExpression exp = (PsiReferenceExpression) reference;
                        PsiElement parent = exp.getParent();
                        if (parent instanceof PsiMethodCallExpression) {
                            PsiMethodCallExpression psiMethodCallExpression = (PsiMethodCallExpression) parent;
                            ProblemDescriptor problemDescriptor = xxx(psiMethodCallExpression, manager, project);
                            if (problemDescriptor != null) {
                                list.add(problemDescriptor);
                            }
                        }
                    }
                }
            }
        }
        return list.toArray(new ProblemDescriptor[list.size()]);
    }

    private ProblemDescriptor xxx(PsiMethodCallExpression psiMethodCallExpression, InspectionManager manager, Project project) {
        PsiManager psiManager = psiMethodCallExpression.getManager();
        PsiExpressionList argumentList = psiMethodCallExpression.getArgumentList();
        PsiExpression[] expressions = argumentList.getExpressions();
        PsiType[] paramTypes = null;
        if (expressions.length == 2) {
            paramTypes = getTypes(expressions[1], false);
        } else if (expressions.length == 1) {
            paramTypes = new PsiType[0];
        }
        if (paramTypes != null) {
            PsiExpression classExpression = expressions[0];
            return createProblemDescriptor(classExpression, psiManager, project, paramTypes, psiMethodCallExpression, manager);
        }
        return null;
    }

    private ProblemDescriptor createProblemDescriptor(PsiExpression classExpression, PsiManager psiManager, Project project,
                                                      PsiType[] paramTypes, PsiMethodCallExpression psiMethodCallExpression, InspectionManager manager) {
        if (classExpression instanceof PsiClassObjectAccessExpression) {
            PsiClassObjectAccessExpression coae = (PsiClassObjectAccessExpression) classExpression;
            PsiTypeElement operand = coae.getOperand();
            PsiType type = operand.getType();
            PsiClass aClass2 = psiManager.findClass(type.getCanonicalText(), GlobalSearchScope.allScope(project));
            if (aClass2 == null) {
                logger.info("Can't find class " + type.getCanonicalText());
                return null;
            }
            PsiMethod[] constructors = aClass2.getConstructors();
            if (constructors.length == 0 && paramTypes.length == 0) {
                return null;
            }
            for (PsiMethod constructor : constructors) {
                if (constructor.getModifierList().hasModifierProperty("public")) {
                    PsiParameterList parameterList = constructor.getParameterList();
                    PsiParameter[] parameters = parameterList.getParameters();
                    if (checkParameters(parameters, paramTypes, project)) {
                        return null;
                    }
                }
            }
            String message = getMessage(type, paramTypes);
            return manager.createProblemDescriptor(psiMethodCallExpression,
                    message, (LocalQuickFix) null,
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
        }
        return null;
    }

    private boolean checkParameters(PsiParameter[] parameters, PsiType[] paramTypes, Project project) {
        if (parameters.length == paramTypes.length) {
            for (int i = 0; i < parameters.length; i++) {
                PsiParameter parameter = parameters[i];
                PsiType paramType = parameter.getType();
                if (paramType instanceof PsiPrimitiveType) {
                    PsiPrimitiveType psiPrimitiveType = (PsiPrimitiveType) paramType;
                    paramType = getBoxedType(psiPrimitiveType, parameter.getManager(), project);
                }
                if (paramType == null || paramTypes[i] == null || !paramType.isAssignableFrom(paramTypes[i])) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    private String getMessage(PsiType type, PsiType[] paramTypes) {
        StringBuffer sb = new StringBuffer();
        sb.append(type.getCanonicalText());
        sb.append(" does not have a");
        if (paramTypes.length == 0) {
            sb.append("n empty constructor.");
        } else if (paramTypes.length == 1) {
            sb.append(" constructor of type ").append(getTypeName(paramTypes[0]));
        } else {
            sb.append(" constructor with types ");
            appendTypes(paramTypes, sb);
        }
        return sb.toString();
    }

    private void appendTypes(PsiType[] paramTypes, StringBuffer sb) {
        for (int i = 0; i < paramTypes.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(getTypeName(paramTypes[i]));
        }
    }

    private String getTypeName(PsiType paramType) {
        return paramType == null ? "unknown" : paramType.getCanonicalText();
    }

    private PsiType[] getTypes(PsiExpression params, boolean classes) {
        if (params instanceof PsiReferenceExpression) {
            PsiReferenceExpression psiReferenceExpression = (PsiReferenceExpression) params;
            PsiElement psiElement = psiReferenceExpression.resolve();
            if (psiElement instanceof PsiLocalVariable) {
                PsiLocalVariable psiLocalVariable = (PsiLocalVariable) psiElement;
                params = psiLocalVariable.getInitializer();
            } else if (psiElement instanceof PsiField) {
                PsiField psiField = (PsiField) psiElement;
                params = psiField.getInitializer();
            }
        }
        if (params instanceof PsiClassObjectAccessExpression && classes) {
            PsiClassObjectAccessExpression psiClassObjectAccessExpression = (PsiClassObjectAccessExpression) params;
            PsiType type = psiClassObjectAccessExpression.getOperand().getType();
            return new PsiType[]{type};
        }
        if (params instanceof PsiNewExpression) {
            PsiNewExpression psiNewExpression = (PsiNewExpression) params;
            params = psiNewExpression.getArrayInitializer();
            if (params == null && !classes) {
                return new PsiType[]{psiNewExpression.getType()};
            }
        }
        if (params instanceof PsiArrayInitializerExpression) {
            PsiArrayInitializerExpression arrayInitializer = (PsiArrayInitializerExpression) params;
            PsiExpression[] initializers = arrayInitializer.getInitializers();
            PsiType[] paramTypes = new PsiType[initializers.length];
            for (int k = 0; k < initializers.length; k++) {
                PsiExpression initializer = initializers[k];
                if (initializer instanceof PsiClassObjectAccessExpression && classes) {
                    PsiClassObjectAccessExpression psiClassObjectAccessExpression = (PsiClassObjectAccessExpression) initializer;
                    paramTypes[k] = psiClassObjectAccessExpression.getOperand().getType();
                } else if (!classes) {
                    paramTypes[k] = initializer.getType();
                }
            }
            return paramTypes;
        }
        if (params != null) {
            return new PsiType[]{params.getType()};
        }
        return null;
    }

    private PsiType getBoxedType(PsiPrimitiveType psiPrimitiveType, PsiManager manager, Project project) {
        String s = psiPrimitiveType.getCanonicalText();
        String className = "java.lang." + (s.equals("int") ? "Integer" : s.substring(0, 1).toUpperCase() + s.substring(1));
        return manager.getElementFactory().createTypeByFQClassName(className, GlobalSearchScope.allScope(project));
    }
}
